# Build Script for Lianstar-RS by clonne/2023
# WHY THE WORLD CAN'T FOUND A MAKE-TOOLS FOR EASY TO USE?
# FINALLY CAN ONLY I WRITE A LITTLE MAKE-TOOLS
# THANK PYTHON, IT'S EASY TO USE VERY!

################################################################
# SCRIPT AREA BEGIN
################################################################

ROOT = {
    "name": "itags",
    "!target-dir": "_dist",
    "items": [
        {
            "name": "server",
            "deps": ["server/*.ts"],
            "!target-name": "server.exe",
            "on-update": ["order",
                ["deno", "compile", "-A", "--output", "{@target-path}", "server/main.ts"],
            ],
        },
        {
            "name": "ux",
            "deps": ["ux/*.ts"],
            "!target-name": "ux.js",
            "on-update": ["order",
                ["deno", "bundle", "ux/main.ts", ">", "{@target-path}"],
            ],
        },
    ],
}

################################################################
# SCRIPT AREA HAS END :) NOW CAN python build.py
################################################################

from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
import subprocess

class Const:
    UNNAMED = "<!NONAME!>"
    HEAD_INTERNAL_VAR = '!'
    HEAD_CUSTOM_VAR = '.'
    HEAD_AUTO_VAR = '@'
    HEAD_INTERNAL_COMMAND = '<'
    TAB_SIZE = 2
    V_TARGET_DIR = "!target-dir"
    V_TARGET_NAME = "!target-name"
    V_TARGET_PATH = "@target-path"
    UPDATE_TYPE_ORDER = "ORDER"
    COMMAND_COPY = "<COPY>"

class _m:
    _log_indent = 0

def log_indent(n:int) -> str:
    return (' ' * (n * Const.TAB_SIZE))

def log_push() -> None:
    _m._log_indent += 1

def log_pop() -> None:
    _m._log_indent = max(0, _m._log_indent-1)

def log_info(text:str) -> None:
    print(f"{log_indent(_m._log_indent)}{text}")

def log_warning(text:str) -> None:
    log_info(f"[!] {text}")

def log_error(text:str) -> None:
    log_info(f"[X] {text}")

@dataclass
class Envirom:
    vars:dict

def env_default() -> Envirom: return Envirom(dict())

def env_copy(source:Envirom) -> Envirom:
    return Envirom(source.vars.copy())

def env_var(env:Envirom, name:str) -> str:
    return str(env.vars.get(name, ""))

def env_add_var(env:Envirom, name:str, value:str) -> Envirom:
    env.vars[name] = value
    return env

def env_solve_value(env:Envirom, raw_value:str) -> str:
    raw_size = len(raw_value)
    def on_brece(i:int, take:str="") -> tuple:
        if i < raw_size:
            if '}' == raw_value[i]:
                return (env_var(env, take), i+1)
            else:
                return on_brece(1+i, take+raw_value[i])
        else:
            return ("", i)
    units = list()
    i = 0
    while i < raw_size:
        if '{' == raw_value[i]:
            take, i = on_brece(1+i)
            if len(take) > 0: units.append(take)
        else:
            units.append(raw_value[i])
            i += 1
    return (''.join(units))

def env_make_auto_vars(env:Envirom) -> Envirom:
    env_add_var(env, Const.V_TARGET_PATH, str(Path(env_var(env, Const.V_TARGET_DIR), env_var(env, Const.V_TARGET_NAME))))
    return env

def env_make(node:dict, env_super:Envirom) -> Envirom:
    env = env_copy(env_super)
    for k,v in node.items():
        if isinstance(k,str) and (len(k) > 0):
            if k[0] in (Const.HEAD_INTERNAL_VAR, Const.HEAD_CUSTOM_VAR):
                env_add_var(env, k, env_solve_value(env, v))
    return env_make_auto_vars(env)

def maxtime_by_path(path:Path) -> datetime|None:
    if path.is_file():
        max_time = max(path.stat().st_ctime, path.stat().st_mtime)
        return datetime.fromtimestamp(max_time)
    return None

def maxtime_by_ab(a:datetime, b:datetime) -> datetime|None:
    if isinstance(a,datetime):
        if isinstance(b,datetime):
            return a if (a > b) else b
        return a
    return b

def maxtime_by_deps(deps:list) -> datetime|None:
    maxtime = None
    for pat in deps:
        pat_path = Path(str(pat))
        if pat_path.is_file():
            maxtime = maxtime_by_ab(maxtime, maxtime_by_path(pat_path))
        elif pat_path.parent.is_dir():
            child_paths = pat_path.parent.glob(pat_path.name)
            for path in child_paths:
                maxtime = maxtime_by_ab(maxtime, maxtime_by_path(path))
        else:
            log_error(f"deps-path: '{pat}' can't match")
            return None
    return maxtime

def expand_tareget_path(raw:str) -> Path:
    path = Path(raw)
    if not path.is_file():
        path.parent.mkdir(parents=True, exist_ok=True)
    return path

def solve_items(items:list, env:Envirom) -> list:
    return list(map((lambda a:env_solve_value(env, a)), items))

def make_command_shell(args:list):
    program_path = Path(args[0])
    def fn(env:Envirom) -> bool:
        final_args = solve_items(args[1:], env)
        log_info(f"<SHELL> {program_path} {final_args}")
        r = subprocess.run((' '.join([str(program_path), *final_args])), shell=True, capture_output=True)
        if r.returncode != 0:
            return str(r.stderr if (len(r.stderr) > 0) else r.stdout)
        return True
    return fn

def make_command_copy(args:list) -> str:
    def fn(env:Envirom) -> bool:
        final_args = solve_items(args[1:], env)
        log_info(f"{Const.COMMAND_COPY} {final_args}")
        log_push()
        ret = True
        if len(final_args) == 2:
            source_path = Path(final_args[0])
            if source_path.is_file():
                target_path = Path(final_args[1])
                if target_path.is_dir():
                    target_path.mkdir(parents=True, exist_ok=True)
                    target_path = Path(target_path, source_path.name)
                else:
                    target_path.parent.mkdir(parents=True, exist_ok=True)
                try:
                    target_path.write_bytes(source_path.read_bytes())
                    pass
                except:
                    log_error(f"can't write '{target_path}'")
                    ret = False
            else:
                log_error(f"source '{source_path}' not existed")
                ret = False
        else:
            log_error(f"args must be [FROM-FILE, TO-PATH]")
            ret = False
        log_pop()
        return ret
    return fn

def match_command_fn(args:list):
    if len(args) > 0:
        name = str(args[0]).strip().upper()
        if Const.COMMAND_COPY == name:
            return make_command_copy(args)
        else:
            return make_command_shell(args)
    return None

def update_fn_order(doings:list, env:Envirom) -> bool:
    for doing in doings:
        if isinstance(doing, list):
            if len(doing) > 0:
                command_fn = match_command_fn(doing)
                if command_fn is None:
                    log_error(f"on-update:order doing '{doing[0]}' can't match")
                    return False
                else:
                    if not command_fn(env):
                        return False
        else:
            log_error(f"on-update:order doing must be list: '{doing}'")
            return False
    return True

def match_update_fn(type_str:str):
    name = type_str.strip().upper()
    if name == Const.UPDATE_TYPE_ORDER:
        return update_fn_order
    return None

def on_update(items:list, env:Envirom) -> bool:
    if isinstance(items, list):
        if len(items) > 1:
            fn = match_update_fn(items[0])
            if fn is None:
                log_error(f"on-update TYPE '{items[0]}' can't match")
            else:
                return fn(items[1:], env)
        else:
            log_error(f"on-update must be [TYPE, doing...] format")
    else:
        log_error(f"on-update must be list")
    return False

def on_item(node:dict, env:Envirom) -> int:
    name = node.get("name", Const.UNNAMED)
    log_info(f"Item {name}")
    deps_maxtime = maxtime_by_deps(node.get("deps", []))
    if isinstance(deps_maxtime, datetime):
        env = env_make(node, env)
        tareget_path_raw = env_var(env, Const.V_TARGET_PATH)
        target_path = expand_tareget_path(tareget_path_raw)
        if isinstance(target_path, Path):
            target_maxtime = maxtime_by_path(target_path)
            if maxtime_by_ab(target_maxtime, deps_maxtime) == deps_maxtime:
                log_push()
                update_success = on_update(node.get("on-update", []), env)
                if maxtime_by_ab(maxtime_by_path(target_path), deps_maxtime) != deps_maxtime:
                    log_info(f"has update '{target_path}'")
                log_pop()
                return (1 if update_success else 0)
        else:
            log_error(f"{Const.V_TARGET_PATH} '{tareget_path_raw}' can't expand dirs")
    return 0

def on_root(node:dict, env:Envirom):
    name = node.get("name", Const.UNNAMED)
    log_info(f"Project {name}")
    env = env_make(node, env)
    items = node.get("items", [])
    update_count = 0
    if isinstance(items, list):
        log_push()
        for item in items:
            update_count += on_item(item, env)
        log_pop()
        log_info(f"Updated {update_count} / {len(items)}")
    else:
        log_error(f"items must be list")


on_root(ROOT, env_default())
